/*
 * Backstretch
 * http://srobbin.com/jquery-plugins/backstretch/
 *
 * Copyright (c) 2013 Scott Robbin
 * Licensed under the MIT license.
 */

;
(function($, window, undefined) {
	'use strict';

	/** @const */
	var YOUTUBE_REGEXP = /^.*(youtu\.be\/|youtube\.com\/v\/|youtube\.com\/embed\/|youtube\.com\/watch\?v=|youtube\.com\/watch\?.*\&v=)([^#\&\?]*).*/i;

	/* PLUGIN DEFINITION
	 * ========================= */

	$.fn.backstretch = function(images, options) {
		var args = arguments;

		/*
		 * Scroll the page one pixel to get the right window height on iOS
		 * Pretty harmless for everyone else
		 */
		if($(window).scrollTop() === 0) {
			window.scrollTo(0, 0);
		}

		var returnValues;

		this.each(function(eachIndex) {
			var $this = $(this),
				obj = $this.data('backstretch');

			// Do we already have an instance attached to this element?
			if(obj) {

				// Is this a method they're trying to execute?
				if(typeof args[0] === 'string' &&
					typeof obj[args[0]] === 'function') {

					// Call the method
					var returnValue = obj[args[0]].apply(obj, Array.prototype.slice.call(args, 1));
					if(returnValue === obj) { // If a method is chaining
						returnValue = undefined;
					}
					if(returnValue !== undefined) {
						returnValues = returnValues || [];
						returnValues[eachIndex] = returnValue;
					}

					return; // Nothing further to do
				}

				// Merge the old options with the new
				options = $.extend(obj.options, options);

				// Remove the old instance
				if(typeof obj === 'object' && 'destroy' in obj) {
					obj.destroy(true);
				}
			}

			// We need at least one image
			if(!images || (images && images.length === 0)) {
				var cssBackgroundImage = $this.css('background-image');
				if(cssBackgroundImage && cssBackgroundImage !== 'none') {
					images = [{
						url: $this.css('backgroundImage').replace(/url\(|\)|"|'/g, "")
					}];
				} else {
					$.error('No images were supplied for Backstretch, or element must have a CSS-defined background image.');
				}
			}

			obj = new Backstretch(this, images, options || {});
			$this.data('backstretch', obj);
		});

		return returnValues ? returnValues.length === 1 ? returnValues[0] : returnValues : this;
	};

	// If no element is supplied, we'll attach to body
	$.backstretch = function(images, options) {
		// Return the instance
		return $('body')
			.backstretch(images, options)
			.data('backstretch');
	};

	// Custom selector
	$.expr[':'].backstretch = function(elem) {
		return $(elem).data('backstretch') !== undefined;
	};

	/* DEFAULTS
	 * ========================= */

	$.fn.backstretch.defaults = {
		duration: 5000 // Amount of time in between slides (if slideshow)
			,
		transition: 'fade' // Type of transition between slides
			,
		transitionDuration: 0 // Duration of transition between slides
			,
		animateFirst: true // Animate the transition of first image of slideshow in?
			,
		alignX: 0.5 // The x-alignment for the image, can be 'left'|'center'|'right' or any number between 0.0 and 1.0
			,
		alignY: 0.5 // The y-alignment for the image, can be 'top'|'center'|'bottom' or any number between 0.0 and 1.0
			,
		paused: false // Whether the images should slide after given duration
			,
		start: 0 // Index of the first image to show
			,
		preload: 2 // How many images preload at a time?
			,
		preloadSize: 1 // How many images can we preload in parallel?
			,
		resolutionRefreshRate: 2500 // How long to wait before switching resolution?
			,
		resolutionChangeRatioThreshold: 0.1 // How much a change should it be before switching resolution?
	};

	/* STYLES
	 *
	 * Baked-in styles that we'll apply to our elements.
	 * In an effort to keep the plugin simple, these are not exposed as options.
	 * That said, anyone can override these in their own stylesheet.
	 * ========================= */
	var styles = {
		wrap: {
			left: 0,
			top: 0,
			overflow: 'hidden',
			margin: 0,
			padding: 0,
			height: '100%',
			width: '100%',
			zIndex: -999999
		},
		itemWrapper: {
			position: 'absolute',
			display: 'none',
			margin: 0,
			padding: 0,
			border: 'none',
			width: '100%',
			height: '100%',
			zIndex: -999999
		},
		item: {
			position: 'absolute',
			margin: 0,
			padding: 0,
			border: 'none',
			width: '100%',
			height: '100%',
			maxWidth: 'none'
		}
	};

	/* Given an array of different options for an image,
	 * choose the optimal image for the container size.
	 *
	 * Given an image template (a string with {{ width }} and/or
	 * {{height}} inside) and a container object, returns the
	 * image url with the exact values for the size of that
	 * container.
	 *
	 * Returns an array of urls optimized for the specified resolution.
	 *
	 */
	var optimalSizeImages = (function() {

		/* Sorts the array of image sizes based on width */
		var widthInsertSort = function(arr) {
			for(var i = 1; i < arr.length; i++) {
				var tmp = arr[i],
					j = i;
				while(arr[j - 1] && parseInt(arr[j - 1].width, 10) > parseInt(tmp.width, 10)) {
					arr[j] = arr[j - 1];
					--j;
				}
				arr[j] = tmp;
			}

			return arr;
		};

		/* Given an array of various sizes of the same image and a container width,
		 * return the best image.
		 */
		var selectBest = function(containerWidth, containerHeight, imageSizes) {

			var devicePixelRatio = window.devicePixelRatio || 1;
			var deviceOrientation = getDeviceOrientation();
			var windowOrientation = getWindowOrientation();
			var wrapperOrientation = (containerHeight > containerWidth) ?
				'portrait' :
				(containerWidth > containerHeight ? 'landscape' : 'square');

			var lastAllowedImage = 0;
			var testWidth;

			for(var j = 0, image; j < imageSizes.length; j++) {

				image = imageSizes[j];

				// In case a new image was pushed in, process it:
				if(typeof image === 'string') {
					image = imageSizes[j] = {
						url: image
					};
				}

				if(image.pixelRatio && image.pixelRatio !== 'auto' && parseFloat(image.pixelRatio) !== devicePixelRatio) {
					// We disallowed choosing this image for current device pixel ratio,
					// So skip this one.
					continue;
				}

				if(image.deviceOrientation && image.deviceOrientation !== deviceOrientation) {
					// We disallowed choosing this image for current device orientation,
					// So skip this one.
					continue;
				}

				if(image.windowOrientation && image.windowOrientation !== deviceOrientation) {
					// We disallowed choosing this image for current window orientation,
					// So skip this one.
					continue;
				}

				if(image.orientation && image.orientation !== wrapperOrientation) {
					// We disallowed choosing this image for current element's orientation,
					// So skip this one.
					continue;
				}

				// Mark this one as the last one we investigated
				// which does not violate device pixel ratio rules.
				// We may choose this one later if there's no match.
				lastAllowedImage = j;

				// For most images, we match the specified width against element width,
				// And enforcing a limit depending on the "pixelRatio" property if specified.
				// But if a pixelRatio="auto", then we consider the width as the physical width of the image,
				// And match it while considering the device's pixel ratio.
				testWidth = containerWidth;
				if(image.pixelRatio === 'auto') {
					containerWidth *= devicePixelRatio;
				}

				// Stop when the width of the image is larger or equal to the container width
				if(image.width >= testWidth) {
					break;
				}
			}

			// Use the image located at where we stopped
			return imageSizes[Math.min(j, lastAllowedImage)];
		};

		var replaceTagsInUrl = function(url, templateReplacer) {

			if(typeof url === 'string') {
				url = url.replace(/{{(width|height)}}/g, templateReplacer);
			} else if(url instanceof Array) {
				for(var i = 0; i < url.length; i++) {
					if(url[i].src) {
						url[i].src = replaceTagsInUrl(url[i].src, templateReplacer);
					} else {
						url[i] = replaceTagsInUrl(url[i], templateReplacer);
					}
				}
			}

			return url;
		};

		return function($container, images) {
			var containerWidth = $container.width(),
				containerHeight = $container.height();

			var chosenImages = [];

			var templateReplacer = function(match, key) {
				if(key === 'width') {
					return containerWidth;
				}
				if(key === 'height') {
					return containerHeight;
				}
				return match;
			};

			for(var i = 0; i < images.length; i++) {
				if($.isArray(images[i])) {
					images[i] = widthInsertSort(images[i]);
					var chosen = selectBest(containerWidth, containerHeight, images[i]);
					chosenImages.push(chosen);
				} else {
					// In case a new image was pushed in, process it:
					if(typeof images[i] === 'string') {
						images[i] = {
							url: images[i]
						};
					}

					var item = $.extend({}, images[i]);
					item.url = replaceTagsInUrl(item.url, templateReplacer);
					chosenImages.push(item);
				}
			}
			return chosenImages;
		};

	})();

	var isVideoSource = function(source) {
		return YOUTUBE_REGEXP.test(source.url) || source.isVideo;
	};

	/* Preload images */
	var preload = (function(sources, startAt, count, batchSize, callback) {
		// Plugin cache
		var cache = [];

		// Wrapper for cache
		var caching = function(image) {
			for(var i = 0; i < cache.length; i++) {
				if(cache[i].src === image.src) {
					return cache[i];
				}
			}
			cache.push(image);
			return image;
		};

		// Execute callback
		var exec = function(sources, callback, last) {
			if(typeof callback === 'function') {
				callback.call(sources, last);
			}
		};

		// Closure to hide cache
		return function preload(sources, startAt, count, batchSize, callback) {
			// Check input data
			if(typeof sources === 'undefined') {
				return;
			}
			if(!$.isArray(sources)) {
				sources = [sources];
			}

			if(arguments.length < 5 && typeof arguments[arguments.length - 1] === 'function') {
				callback = arguments[arguments.length - 1];
			}

			startAt = (typeof startAt === 'function' || !startAt) ? 0 : startAt;
			count = (typeof count === 'function' || !count || count < 0) ? sources.length : Math.min(count, sources.length);
			batchSize = (typeof batchSize === 'function' || !batchSize) ? 1 : batchSize;

			if(startAt >= sources.length) {
				startAt = 0;
				count = 0;
			}
			if(batchSize < 0) {
				batchSize = count;
			}
			batchSize = Math.min(batchSize, count);

			var next = sources.slice(startAt + batchSize, count - batchSize);
			sources = sources.slice(startAt, batchSize);
			count = sources.length;

			// If sources array is empty
			if(!count) {
				exec(sources, callback, true);
				return;
			}

			// Image loading callback
			var countLoaded = 0;

			var loaded = function() {
				countLoaded++;
				if(countLoaded !== count) {
					return;
				}

				exec(sources, callback, !next);
				preload(next, 0, 0, batchSize, callback);
			};

			// Loop sources to preload
			var image;

			for(var i = 0; i < sources.length; i++) {

				if(isVideoSource(sources[i])) {

					// Do not preload videos. There are issues with that.
					// First - we need to keep an instance of the preloaded and use that exactly, not a copy.
					// Second - there are memory issues.
					// If there will be a requirement from users - I'll try to implement this.

					continue;

				} else {

					image = new Image();
					image.src = sources[i].url;

					image = caching(image);

					if(image.complete) {
						loaded();
					} else {
						$(image).on('load error', loaded);
					}

				}

			}
		};
	})();

	/* Process images array */
	var processImagesArray = function(images) {
		var processed = [];
		for(var i = 0; i < images.length; i++) {
			if(typeof images[i] === 'string') {
				processed.push({
					url: images[i]
				});
			} else if($.isArray(images[i])) {
				processed.push(processImagesArray(images[i]));
			} else {
				processed.push(processOptions(images[i]));
			}
		}
		return processed;
	};

	/* Process options */
	var processOptions = function(options, required) {

		// Convert old options

		// centeredX/centeredY are deprecated
		if(options.centeredX || options.centeredY) {
			if(window.console && window.console.log) {
				window.console.log('jquery.backstretch: `centeredX`/`centeredY` is deprecated, please use `alignX`/`alignY`');
			}
			if(options.centeredX) {
				options.alignX = 0.5;
			}
			if(options.centeredY) {
				options.alignY = 0.5;
			}
		}

		// Deprecated spec
		if(options.speed !== undefined) {

			if(window.console && window.console.log) {
				window.console.log('jquery.backstretch: `speed` is deprecated, please use `transitionDuration`');
			}

			options.transitionDuration = options.speed;
			options.transition = 'fade';
		}

		// Typo
		if(options.resolutionChangeRatioTreshold !== undefined) {
			window.console.log('jquery.backstretch: `treshold` is a typo!');
			options.resolutionChangeRatioThreshold = options.resolutionChangeRatioTreshold;
		}

		// Current spec that needs processing

		if(options.fadeFirst !== undefined) {
			options.animateFirst = options.fadeFirst;
		}

		if(options.fade !== undefined) {
			options.transitionDuration = options.fade;
			options.transition = 'fade';
		}

		if(options.scale) {
			options.scale = validScale(options.scale);
		}

		return processAlignOptions(options);
	};

	/* Process align options */
	var processAlignOptions = function(options, required) {
		if(options.alignX === 'left') {
			options.alignX = 0.0;
		} else if(options.alignX === 'center') {
			options.alignX = 0.5;
		} else if(options.alignX === 'right') {
			options.alignX = 1.0;
		} else {
			if(options.alignX !== undefined || required) {
				options.alignX = parseFloat(options.alignX);
				if(isNaN(options.alignX)) {
					options.alignX = 0.5;
				}
			}
		}

		if(options.alignY === 'top') {
			options.alignY = 0.0;
		} else if(options.alignY === 'center') {
			options.alignY = 0.5;
		} else if(options.alignY === 'bottom') {
			options.alignY = 1.0;
		} else {
			if(options.alignX !== undefined || required) {
				options.alignY = parseFloat(options.alignY);
				if(isNaN(options.alignY)) {
					options.alignY = 0.5;
				}
			}
		}

		return options;
	};

	var SUPPORTED_SCALE_OPTIONS = {
		'cover': 'cover',
		'fit': 'fit',
		'fit-smaller': 'fit-smaller',
		'fill': 'fill'
	};

	function validScale(scale) {
		if(!SUPPORTED_SCALE_OPTIONS.hasOwnProperty(scale)) {
			return 'cover';
		}
		return scale;
	}

	/* CLASS DEFINITION
	 * ========================= */
	var Backstretch = function(container, images, options) {
		this.options = $.extend({}, $.fn.backstretch.defaults, options || {});

		this.firstShow = true;

		// Process options
		processOptions(this.options, true);

		/* In its simplest form, we allow Backstretch to be called on an image path.
		 * e.g. $.backstretch('/path/to/image.jpg')
		 * So, we need to turn this back into an array.
		 */
		this.images = processImagesArray($.isArray(images) ? images : [images]);

		/**
		 * Paused-Option
		 */
		if(this.options.paused) {
			this.paused = true;
		}

		/**
		 * Start-Option (Index)
		 */
		if(this.options.start >= this.images.length) {
			this.options.start = this.images.length - 1;
		}
		if(this.options.start < 0) {
			this.options.start = 0;
		}

		// Convenience reference to know if the container is body.
		this.isBody = container === document.body;

		/* We're keeping track of a few different elements
		 *
		 * Container: the element that Backstretch was called on.
		 * Wrap: a DIV that we place the image into, so we can hide the overflow.
		 * Root: Convenience reference to help calculate the correct height.
		 */
		var $window = $(window);
		this.$container = $(container);
		this.$root = this.isBody ? supportsFixedPosition ? $window : $(document) : this.$container;

		this.originalImages = this.images;
		this.images = optimalSizeImages(
			this.options.alwaysTestWindowResolution ? $window : this.$root,
			this.originalImages);

		/**
		 * Pre-Loading.
		 * This is the first image, so we will preload a minimum of 1 images.
		 */
		preload(this.images, this.options.start || 0, this.options.preload || 1);

		// Don't create a new wrap if one already exists (from a previous instance of Backstretch)
		var $existing = this.$container.children(".backstretch").first();
		this.$wrap = $existing.length ? $existing :
			$('<div class="backstretch"></div>')
			.css(this.options.bypassCss ? {} : styles.wrap)
			.appendTo(this.$container);

		if(!this.options.bypassCss) {

			// Non-body elements need some style adjustments
			if(!this.isBody) {
				// If the container is statically positioned, we need to make it relative,
				// and if no zIndex is defined, we should set it to zero.
				var position = this.$container.css('position'),
					zIndex = this.$container.css('zIndex');

				this.$container.css({
					position: position === 'static' ? 'relative' : position,
					zIndex: zIndex === 'auto' ? 0 : zIndex
				});

				// Needs a higher z-index
				this.$wrap.css({
					zIndex: -999998
				});
			}

			// Fixed or absolute positioning?
			this.$wrap.css({
				position: this.isBody && supportsFixedPosition ? 'fixed' : 'absolute'
			});

		}

		// Set the first image
		this.index = this.options.start;
		this.show(this.index);

		// Listen for resize
		$window.on('resize.backstretch', $.proxy(this.resize, this))
			.on('orientationchange.backstretch', $.proxy(function() {
				// Need to do this in order to get the right window height
				if(this.isBody && window.pageYOffset === 0) {
					window.scrollTo(0, 1);
					this.resize();
				}
			}, this));
	};

	var performTransition = function(options) {

		var transition = options.transition || 'fade';

		// Look for multiple options
		if(typeof transition === 'string' && transition.indexOf('|') > -1) {
			transition = transition.split('|');
		}

		if(transition instanceof Array) {
			transition = transition[Math.round(Math.random() * (transition.length - 1))];
		}

		var $new = options['new'];
		var $old = options['old'] ? options['old'] : $([]);

		switch(transition.toString().toLowerCase()) {

			default:
				case 'fade':
				$new.fadeIn({
				duration: options.duration,
				complete: options.complete,
				easing: options.easing || undefined
			});
			break;

			case 'fadeinout':
					case 'fade_in_out':

					var fadeInNew = function() {
						$new.fadeIn({
							duration: options.duration / 2,
							complete: options.complete,
							easing: options.easing || undefined
						});
					};

				if($old.length) {
					$old.fadeOut({
						duration: options.duration / 2,
						complete: fadeInNew,
						easing: options.easing || undefined
					});
				} else {
					fadeInNew();
				}

				break;

			case 'pushleft':
					case 'push_left':
					case 'pushright':
					case 'push_right':
					case 'pushup':
					case 'push_up':
					case 'pushdown':
					case 'push_down':
					case 'coverleft':
					case 'cover_left':
					case 'coverright':
					case 'cover_right':
					case 'coverup':
					case 'cover_up':
					case 'coverdown':
					case 'cover_down':

					var transitionParts = transition.match(/^(cover|push)_?(.*)$/);

				var animProp = transitionParts[2] === 'left' ? 'right' : transitionParts[2] === 'right' ? 'left' : transitionParts[2] === 'down' ? 'top' : transitionParts[2] === 'up' ? 'bottom' : 'right';

				var newCssStart = {
						'display': ''
					},
					newCssAnim = {};
				newCssStart[animProp] = '-100%';
				newCssAnim[animProp] = 0;

				$new
				.css(newCssStart)
				.animate(newCssAnim, {
					duration: options.duration,
					complete: function() {
						$new.css(animProp, '');
						options.complete.apply(this, arguments);
					},
					easing: options.easing || undefined
				});

				if(transitionParts[1] === 'push' && $old.length) {
					var oldCssAnim = {};
					oldCssAnim[animProp] = '100%';

					$old
						.animate(oldCssAnim, {
							duration: options.duration,
							complete: function() {
								$old.css('display', 'none');
							},
							easing: options.easing || undefined
						});
				}

				break;
		}

	};

	/* PUBLIC METHODS
	 * ========================= */
	Backstretch.prototype = {

		resize: function() {
				try {

					// Check for a better suited image after the resize
					var $resTest = this.options.alwaysTestWindowResolution ? $(window) : this.$root;
					var newContainerWidth = $resTest.width();
					var newContainerHeight = $resTest.height();
					var changeRatioW = newContainerWidth / (this._lastResizeContainerWidth || 0);
					var changeRatioH = newContainerHeight / (this._lastResizeContainerHeight || 0);
					var resolutionChangeRatioThreshold = this.options.resolutionChangeRatioThreshold || 0.0;

					// check for big changes in container size
					if((newContainerWidth !== this._lastResizeContainerWidth ||
							newContainerHeight !== this._lastResizeContainerHeight) &&
						((Math.abs(changeRatioW - 1) >= resolutionChangeRatioThreshold || isNaN(changeRatioW)) ||
							(Math.abs(changeRatioH - 1) >= resolutionChangeRatioThreshold || isNaN(changeRatioH)))) {

						this._lastResizeContainerWidth = newContainerWidth;
						this._lastResizeContainerHeight = newContainerHeight;

						// Big change: rebuild the entire images array
						this.images = optimalSizeImages($resTest, this.originalImages);

						// Preload them (they will be automatically inserted on the next cycle)
						if(this.options.preload) {
							preload(this.images, (this.index + 1) % this.images.length, this.options.preload);
						}

						// In case there is no cycle and the new source is different than the current
						if(this.images.length === 1 &&
							this._currentImage.url !== this.images[0].url) {

							// Wait a little an update the image being showed
							var that = this;
							clearTimeout(that._selectAnotherResolutionTimeout);
							that._selectAnotherResolutionTimeout = setTimeout(function() {
								that.show(0);
							}, this.options.resolutionRefreshRate);
						}
					}

					var bgCSS = {
							left: 0,
							top: 0,
							right: 'auto',
							bottom: 'auto'
						}

						,
						boxWidth = this.isBody ? this.$root.width() : this.$root.innerWidth(),
						boxHeight = this.isBody ?
						(window.innerHeight ? window.innerHeight : this.$root.height()) :
						this.$root.innerHeight()

						,
						naturalWidth = this.$itemWrapper.data('width'),
						naturalHeight = this.$itemWrapper.data('height')

						,
						ratio = (naturalWidth / naturalHeight) || 1

						,
						alignX = this._currentImage.alignX === undefined ? this.options.alignX : this._currentImage.alignX,
						alignY = this._currentImage.alignY === undefined ? this.options.alignY : this._currentImage.alignY,
						scale = validScale(this._currentImage.scale || this.options.scale);

					var width, height;

					if(scale === 'fit' || scale === 'fit-smaller') {
						width = naturalWidth;
						height = naturalHeight;

						if(width > boxWidth ||
							height > boxHeight ||
							scale === 'fit-smaller') {
							var boxRatio = boxWidth / boxHeight;
							if(boxRatio > ratio) {
								width = Math.floor(boxHeight * ratio);
								height = boxHeight;
							} else if(boxRatio < ratio) {
								width = boxWidth;
								height = Math.floor(boxWidth / ratio);
							} else {
								width = boxWidth;
								height = boxHeight;
							}
						}
					} else if(scale === 'fill') {
						width = boxWidth;
						height = boxHeight;
					} else { // 'cover'
						width = Math.max(boxHeight * ratio, boxWidth);
						height = Math.max(width / ratio, boxHeight);
					}

					// Make adjustments based on image ratio
					bgCSS.top = -(height - boxHeight) * alignY;
					bgCSS.left = -(width - boxWidth) * alignX;
					bgCSS.width = width;
					bgCSS.height = height;

					if(!this.options.bypassCss) {

						this.$wrap
							.css({
								width: boxWidth,
								height: boxHeight
							})
							.find('>.backstretch-item').not('.deleteable')
							.each(function() {
								var $wrapper = $(this);
								$wrapper.find('img,video,iframe')
									.css(bgCSS);
							});
					}

					var evt = $.Event('backstretch.resize', {
						relatedTarget: this.$container[0]
					});
					this.$container.trigger(evt, this);

				} catch(err) {
					// IE7 seems to trigger resize before the image is loaded.
					// This try/catch block is a hack to let it fail gracefully.
				}

				return this;
			}

			// Show the slide at a certain position
			,
		show: function(newIndex, overrideOptions) {

				// Validate index
				if(Math.abs(newIndex) > this.images.length - 1) {
					return;
				}

				// Vars
				var that = this,
					$oldItemWrapper = that.$wrap.find('>.backstretch-item').addClass('deleteable'),
					oldVideoWrapper = that.videoWrapper,
					evtOptions = {
						relatedTarget: that.$container[0]
					};

				// Trigger the "before" event
				that.$container.trigger($.Event('backstretch.before', evtOptions), [that, newIndex]);

				// Set the new frame index
				this.index = newIndex;
				var selectedImage = that.images[newIndex];

				// Pause the slideshow
				clearTimeout(that._cycleTimeout);

				// New image

				delete that.videoWrapper; // Current item may not be a video

				var isVideo = isVideoSource(selectedImage);
				if(isVideo) {
					that.videoWrapper = new VideoWrapper(selectedImage);
					that.$item = that.videoWrapper.$video.css('pointer-events', 'none');
				} else {
					that.$item = $('<img />');
				}

				that.$itemWrapper = $('<div class="backstretch-item">')
					.append(that.$item);

				if(this.options.bypassCss) {
					that.$itemWrapper.css({
						'display': 'none'
					});
				} else {
					that.$itemWrapper.css(styles.itemWrapper);
					that.$item.css(styles.item);
				}

				that.$item.bind(isVideo ? 'canplay' : 'load', function(e) {
					var $this = $(this),
						$wrapper = $this.parent(),
						options = $wrapper.data('options');

					if(overrideOptions) {
						options = $.extend({}, options, overrideOptions);
					}

					var imgWidth = this.naturalWidth || this.videoWidth || this.width,
						imgHeight = this.naturalHeight || this.videoHeight || this.height;

					// Save the natural dimensions
					$wrapper
						.data('width', imgWidth)
						.data('height', imgHeight);

					var getOption = function(opt) {
						return options[opt] !== undefined ?
							options[opt] :
							that.options[opt];
					};

					var transition = getOption('transition');
					var transitionEasing = getOption('transitionEasing');
					var transitionDuration = getOption('transitionDuration');

					// Show the image, then delete the old one
					var bringInNextImage = function() {

						if(oldVideoWrapper) {
							oldVideoWrapper.stop();
							oldVideoWrapper.destroy();
						}

						$oldItemWrapper.remove();

						// Resume the slideshow
						if(!that.paused && that.images.length > 1) {
							that.cycle();
						}

						// Now we can clear the background on the element, to spare memory
						if(!that.options.bypassCss && !that.isBody) {
							that.$container.css('background-image', 'none');
						}

						// Trigger the "after" and "show" events
						// "show" is being deprecated
						$(['after', 'show']).each(function() {
							that.$container.trigger($.Event('backstretch.' + this, evtOptions), [that, newIndex]);
						});

						if(isVideo) {
							that.videoWrapper.play();
						}
					};

					if((that.firstShow && !that.options.animateFirst) || !transitionDuration || !transition) {
						// Avoid transition on first show or if there's no transitionDuration value
						$wrapper.show();
						bringInNextImage();
					} else {

						performTransition({
							'new': $wrapper,
							old: $oldItemWrapper,
							transition: transition,
							duration: transitionDuration,
							easing: transitionEasing,
							complete: bringInNextImage
						});

					}

					that.firstShow = false;

					// Resize
					that.resize();
				});

				that.$itemWrapper.appendTo(that.$wrap);

				that.$item.attr('alt', selectedImage.alt || '');
				that.$itemWrapper.data('options', selectedImage);

				if(!isVideo) {
					that.$item.attr('src', selectedImage.url);
				}

				that._currentImage = selectedImage;

				return that;
			}

			,
		current: function() {
				return this.index;
			}

			,
		next: function() {
				var args = Array.prototype.slice.call(arguments, 0);
				args.unshift(this.index < this.images.length - 1 ? this.index + 1 : 0);
				return this.show.apply(this, args);
			}

			,
		prev: function() {
				var args = Array.prototype.slice.call(arguments, 0);
				args.unshift(this.index === 0 ? this.images.length - 1 : this.index - 1);
				return this.show.apply(this, args);
			}

			,
		pause: function() {
				// Pause the slideshow
				this.paused = true;

				if(this.videoWrapper) {
					this.videoWrapper.pause();
				}

				return this;
			}

			,
		resume: function() {
				// Resume the slideshow
				this.paused = false;

				if(this.videoWrapper) {
					this.videoWrapper.play();
				}

				this.cycle();
				return this;
			}

			,
		cycle: function() {
				// Start/resume the slideshow
				if(this.images.length > 1) {
					// Clear the timeout, just in case
					clearTimeout(this._cycleTimeout);

					var duration = (this._currentImage && this._currentImage.duration) || this.options.duration;
					var isVideo = isVideoSource(this._currentImage);

					var callNext = function() {
						this.$item.off('.cycle');

						// Check for paused slideshow
						if(!this.paused) {
							this.next();
						}
					};

					// Special video handling
					if(isVideo) {

						// Leave video at last frame
						if(!this._currentImage.loop) {
							var lastFrameTimeout = 0;

							this.$item
								.on('playing.cycle', function() {
									var player = $(this).data('player');

									clearTimeout(lastFrameTimeout);
									lastFrameTimeout = setTimeout(function() {
										player.pause();
										player.$video.trigger('ended');
									}, (player.getDuration() - player.getCurrentTime()) * 1000);
								})
								.on('ended.cycle', function() {
									clearTimeout(lastFrameTimeout);
								});
						}

						// On error go to next
						this.$item.on('error.cycle initerror.cycle', $.proxy(callNext, this));
					}

					if(isVideo && !this._currentImage.duration) {
						// It's a video - playing until end
						this.$item.on('ended.cycle', $.proxy(callNext, this));

					} else {
						// Cycling according to specified duration
						this._cycleTimeout = setTimeout($.proxy(callNext, this), duration);
					}

				}
				return this;
			}

			,
		destroy: function(preserveBackground) {
			// Stop the resize events
			$(window).off('resize.backstretch orientationchange.backstretch');

			// Stop any videos
			if(this.videoWrapper) {
				this.videoWrapper.destroy();
			}

			// Clear the timeout
			clearTimeout(this._cycleTimeout);

			// Remove Backstretch
			if(!preserveBackground) {
				this.$wrap.remove();
			}
			this.$container.removeData('backstretch');
		}
	};

	/**
	 * Video Abstraction Layer
	 *
	 * Static methods:
	 * > VideoWrapper.loadYoutubeAPI() -> Call in order to load the Youtube API.
	 *                                   An 'youtube_api_load' event will be triggered on $(window) when the API is loaded.
	 *
	 * Generic:
	 * > player.type -> type of the video
	 * > player.video / player.$video -> contains the element holding the video
	 * > player.play() -> plays the video
	 * > player.pause() -> pauses the video
	 * > player.setCurrentTime(position) -> seeks to a position by seconds
	 *
	 * Youtube:
	 * > player.ytId will contain the youtube ID if the source is a youtube url
	 * > player.ytReady is a flag telling whether the youtube source is ready for playback
	 * */

	var VideoWrapper = function() {
		this.init.apply(this, arguments);
	};

	/**
	 * @param {Object} options
	 * @param {String|Array<String>|Array<{{src: String, type: String?}}>} options.url
	 * @param {Boolean} options.loop=false
	 * @param {Boolean?} options.mute=true
	 * @param {String?} options.poster
	 * loop, mute, poster
	 */
	VideoWrapper.prototype.init = function(options) {

		var that = this;

		var $video;

		var setVideoElement = function() {
			that.$video = $video;
			that.video = $video[0];
		};

		// Determine video type

		var videoType = 'video';

		if(!(options.url instanceof Array) &&
			YOUTUBE_REGEXP.test(options.url)) {
			videoType = 'youtube';
		}

		that.type = videoType;

		if(videoType === 'youtube') {

			// Try to load the API in the meantime
			VideoWrapper.loadYoutubeAPI();

			that.ytId = options.url.match(YOUTUBE_REGEXP)[2];
			var src = 'https://www.youtube.com/embed/' + that.ytId +
				'?rel=0&autoplay=0&showinfo=0&controls=0&modestbranding=1' +
				'&cc_load_policy=0&disablekb=1&iv_load_policy=3&loop=0' +
				'&enablejsapi=1&origin=' + encodeURIComponent(window.location.origin);

			that.__ytStartMuted = !!options.mute || options.mute === undefined;

			$video = $('<iframe />')
				.attr({
					'src_to_load': src
				})
				.css({
					'border': 0,
					'margin': 0,
					'padding': 0
				})
				.data('player', that);

			if(options.loop) {
				$video.on('ended.loop', function() {
					if(!that.__manuallyStopped) {
						that.play();
					}
				});
			}

			that.ytReady = false;

			setVideoElement();

			if(window['YT'] && window['YT'].loaded) {
				that._initYoutube();
				$video.trigger('initsuccess');
			} else {
				$(window).one('youtube_api_load', function() {
					that._initYoutube();
					$video.trigger('initsuccess');
				});
			}

		} else {
			// Traditional <video> tag with multiple sources

			$video = $('<video>')
				.prop('autoplay', false)
				.prop('controls', false)
				.prop('loop', !!options.loop)
				.prop('muted', !!options.mute || options.mute === undefined)

				// Let the first frames be available before playback, as we do transitions
				.prop('preload', 'auto')
				.prop('poster', options.poster || '');

			var sources = (options.url instanceof Array) ? options.url : [options.url];

			for(var i = 0; i < sources.length; i++) {
				var sourceItem = sources[i];
				if(typeof(sourceItem) === 'string') {
					sourceItem = {
						src: sourceItem
					};
				}
				$('<source>')
					.attr('src', sourceItem.src)
					// Make sure to not specify type if unknown -
					//   so the browser will try to autodetect.
					.attr('type', sourceItem.type || null)
					.appendTo($video);
			}

			if(!$video[0].canPlayType || !sources.length) {
				$video.trigger('initerror');
			} else {
				$video.trigger('initsuccess');
			}

			setVideoElement();
		}

	};

	VideoWrapper.prototype._initYoutube = function() {
		var that = this;

		var YT = window['YT'];

		that.$video
			.attr('src', that.$video.attr('src_to_load'))
			.removeAttr('src_to_load');

		// It won't init if it's not in the DOM, so we emulate that
		var hasParent = !!that.$video[0].parentNode;
		if(!hasParent) {
			var $tmpParent = $('<div>').css('display', 'none !important').appendTo(document.body);
			that.$video.appendTo($tmpParent);
		}

		var player = new YT.Player(that.video, {
			events: {
				'onReady': function() {

					if(that.__ytStartMuted) {
						player.mute();
					}

					if(!hasParent) {
						// Restore parent to old state - without interrupting any changes
						if(that.$video[0].parentNode === $tmpParent[0]) {
							that.$video.detach();
						}
						$tmpParent.remove();
					}

					that.ytReady = true;
					that._updateYoutubeSize();
					that.$video.trigger('canplay');
				},
				'onStateChange': function(event) {
					switch(event.data) {
						case YT.PlayerState.PLAYING:
							that.$video.trigger('playing');
							break;
						case YT.PlayerState.ENDED:
							that.$video.trigger('ended');
							break;
						case YT.PlayerState.PAUSED:
							that.$video.trigger('pause');
							break;
						case YT.PlayerState.BUFFERING:
							that.$video.trigger('waiting');
							break;
						case YT.PlayerState.CUED:
							that.$video.trigger('canplay');
							break;
					}
				},
				'onPlaybackQualityChange': function() {
					that._updateYoutubeSize();
					that.$video.trigger('resize');
				},
				'onError': function(err) {
					that.hasError = true;
					that.$video.trigger({
						'type': 'error',
						'error': err
					});
				}
			}
		});

		that.ytPlayer = player;

		return that;
	};

	VideoWrapper.prototype._updateYoutubeSize = function() {
		var that = this;

		switch(that.ytPlayer.getPlaybackQuality() || 'medium') {
			case 'small':
				that.video.videoWidth = 426;
				that.video.videoHeight = 240;
				break;
			case 'medium':
				that.video.videoWidth = 640;
				that.video.videoHeight = 360;
				break;
			default:
			case 'large':
				that.video.videoWidth = 854;
				that.video.videoHeight = 480;
				break;
			case 'hd720':
				that.video.videoWidth = 1280;
				that.video.videoHeight = 720;
				break;
			case 'hd1080':
				that.video.videoWidth = 1920;
				that.video.videoHeight = 1080;
				break;
			case 'highres':
				that.video.videoWidth = 2560;
				that.video.videoHeight = 1440;
				break;
		}

		return that;
	};

	VideoWrapper.prototype.play = function() {
		var that = this;

		that.__manuallyStopped = false;

		if(that.type === 'youtube') {
			if(that.ytReady) {
				that.$video.trigger('play');
				that.ytPlayer.playVideo();
			}
		} else {
			that.video.play();
		}

		return that;
	};

	VideoWrapper.prototype.pause = function() {
		var that = this;

		that.__manuallyStopped = false;

		if(that.type === 'youtube') {
			if(that.ytReady) {
				that.ytPlayer.pauseVideo();
			}
		} else {
			that.video.pause();
		}

		return that;
	};

	VideoWrapper.prototype.stop = function() {
		var that = this;

		that.__manuallyStopped = true;

		if(that.type === 'youtube') {
			if(that.ytReady) {
				that.ytPlayer.pauseVideo();
				that.ytPlayer.seekTo(0);
			}
		} else {
			that.video.pause();
			that.video.currentTime = 0;
		}

		return that;
	};

	VideoWrapper.prototype.destroy = function() {
		var that = this;

		if(that.ytPlayer) {
			that.ytPlayer.destroy();
		}

		that.$video.remove();

		return that;
	};

	VideoWrapper.prototype.getCurrentTime = function(seconds) {
		var that = this;

		if(that.type === 'youtube') {
			if(that.ytReady) {
				return that.ytPlayer.getCurrentTime();
			}
		} else {
			return that.video.currentTime;
		}

		return 0;
	};

	VideoWrapper.prototype.setCurrentTime = function(seconds) {
		var that = this;

		if(that.type === 'youtube') {
			if(that.ytReady) {
				that.ytPlayer.seekTo(seconds, true);
			}
		} else {
			that.video.currentTime = seconds;
		}

		return that;
	};

	VideoWrapper.prototype.getDuration = function() {
		var that = this;

		if(that.type === 'youtube') {
			if(that.ytReady) {
				return that.ytPlayer.getDuration();
			}
		} else {
			return that.video.duration;
		}

		return 0;
	};

	/**
	 * This will load the youtube API (if not loaded yet)
	 * Use $(window).one('youtube_api_load', ...) to listen for API loaded event
	 */
	VideoWrapper.loadYoutubeAPI = function() {
		if(window['YT'] && window['__yt_load_event_interval__']) {
			return;
		}

		if(!window['YT'] && !$('script[src*=www\\.youtube\\.com\\/iframe_api]').length) {
			$('<script type="text/javascript" src="https://www.youtube.com/iframe_api">').appendTo('body');
		}

		window['__yt_load_event_interval__'] = setInterval(function() {
			if(window['YT'] && window['YT'].loaded) {
				$(window).trigger('youtube_api_load');
				clearTimeout(window['__yt_load_event_interval__']);
				delete window['__yt_load_event_interval__'];
			}
		}, 50);
	};

	var getDeviceOrientation = function() {

		if('matchMedia' in window) {
			if(window.matchMedia("(orientation: portrait)").matches) {
				return 'portrait';
			} else if(window.matchMedia("(orientation: landscape)").matches) {
				return 'landscape';
			}
		}

		if(screen.height > screen.width) {
			return 'portrait';
		}

		// Even square devices have orientation,
		//   but a desktop browser may be too old for `matchMedia`.
		// Defaulting to `landscape` for the VERY rare case of a square desktop screen is good enough.
		return 'landscape';
	};

	var getWindowOrientation = function() {
		if(window.innerHeight > window.innerWidth) {
			return 'portrait';
		}
		if(window.innerWidth > window.innerHeight) {
			return 'landscape';
		}

		return 'square';
	};

	/* SUPPORTS FIXED POSITION?
	 *
	 * Based on code from jQuery Mobile 1.1.0
	 * http://jquerymobile.com/
	 *
	 * In a nutshell, we need to figure out if fixed positioning is supported.
	 * Unfortunately, this is very difficult to do on iOS, and usually involves
	 * injecting content, scrolling the page, etc.. It's ugly.
	 * jQuery Mobile uses this workaround. It's not ideal, but works.
	 *
	 * Modified to detect IE6
	 * ========================= */

	var supportsFixedPosition = (function() {
		var ua = navigator.userAgent,
			platform = navigator.platform
			// Rendering engine is Webkit, and capture major version
			,
			wkmatch = ua.match(/AppleWebKit\/([0-9]+)/),
			wkversion = !!wkmatch && wkmatch[1],
			ffmatch = ua.match(/Fennec\/([0-9]+)/),
			ffversion = !!ffmatch && ffmatch[1],
			operammobilematch = ua.match(/Opera Mobi\/([0-9]+)/),
			omversion = !!operammobilematch && operammobilematch[1],
			iematch = ua.match(/MSIE ([0-9]+)/),
			ieversion = !!iematch && iematch[1];

		return !(
			// iOS 4.3 and older : Platform is iPhone/Pad/Touch and Webkit version is less than 534 (ios5)
			((platform.indexOf("iPhone") > -1 || platform.indexOf("iPad") > -1 || platform.indexOf(
				"iPod") > -1) && wkversion && wkversion < 534) ||

			// Opera Mini
			(window.operamini && ({}).toString.call(window.operamini) === "[object OperaMini]") ||
			(operammobilematch && omversion < 7458) ||

			//Android lte 2.1: Platform is Android and Webkit version is less than 533 (Android 2.2)
			(ua.indexOf("Android") > -1 && wkversion && wkversion < 533) ||

			// Firefox Mobile before 6.0 -
			(ffversion && ffversion < 6) ||

			// WebOS less than 3
			("palmGetResource" in window && wkversion && wkversion < 534) ||

			// MeeGo
			(ua.indexOf("MeeGo") > -1 && ua.indexOf("NokiaBrowser/8.5.0") > -1) ||

			// IE6
			(ieversion && ieversion <= 6)
		);
	}());

}(jQuery, window));